3 UI 事件
3.1 鼠标事件
该事件不仅来自鼠标,也可能是其他兼容性设备模拟鼠标操作(平板、手机)。
3.1.1 常见鼠标事件
鼠标:
-
mousedown/mouseup
: 在元素上点击 / 释放。 -
mouseover/mouseout
: 从一个元素上移入 / 移出。 -
mousemove
: 在元素上的移动就会触发。 -
click
: 鼠标左键触发。在发生mousedown
及mouseup
这两个事件后,会触发该事件。 -
dblclick
: 在短时间内双击同一元素后触发,很少使用。 -
contextmenu
: 鼠标右键按下时触发。- 还有其他打开菜单的方式。比如特定的键盘按键也会触发,因此它不完全是鼠标事件。
3.1.2 事件顺序
鼠标事件的触发之间是有先后顺序的,比如:
- 一次左键单击事件:
mousedown
-->mouseup
-->click
- 一次左键双击事件:
mousedown
-->mouseup
-->click
-->mousedown
-->mouseup
-->click
-->dblclick
3.1.3 事件属性 - 鼠标按钮
点击事件(mousedown
, mouseup
, click
, dblclick
, contextmenu
)都会拥有一个 event.button
属性,用来保存触发事件的鼠标按键状态:
鼠标按键状态 | event.button |
---|---|
左键 (主要按键) | 0 (常见) |
中键 (辅助按键) | 1 |
右键 (次要按键) | 2 (常见) |
X1 键 (后退按键) | 3 |
X2 键 (前进按键) | 4 |
3.1.4 事件属性 - 组合键
鼠标事件包含了组合键信息,以下是事件属性。如果在事件时,按下了相应的按键,则对应会置为 true
。
event.shiftKey
:Shift;event.altKey
:Alt(或对于 Mac 是 Opt);event.ctrlKey
:Ctrl;event.metaKey
:对于 Mac 是 Cmd。
注意:在 Mac 上,通常使用 cmd 代替 ctrl。所以,在判断用户是否按下 ctrl 组合键时,要这样检查:
if (event.ctrlkey || event.metakey)
3.1.5 事件属性 - 坐标
所有的鼠标事件都提供了两种形式的坐标:
- 相对于视口的坐标:
clientX
和clientY
。 - 相对于文档的坐标:
pageX
和pageY
。
3.1.6 干扰
鼠标事件有事会有副作用,在某些界面中可能会出现干扰:
- 双击事件:比如双击一个文本,除了会触发我们设定的
dblclick
事件外,还会选择文本。 - 按下鼠标:在按下鼠标左键,不松开的情况下拖动鼠标,也会触发选中文本。
解决方案,阻止 mousedown
事件中,浏览器的默认行为:
- 使用
return false
:<b ondblclick="alert('Click!')" onmousedown="return false">XXXX</b>
3.1.6.1 防止复制
额外的tips,如何防止浏览器中,用户的复制行为,保护文本不被复制:
<div oncopy="alert('不允许复制!'); return false">
这里是不允许复制的文本内容。
</div>
使用 oncopy
特性,返回 false
,在用户尝试右键点击复制的时候,就会触发 oncopy
中的代码,弹出提示框,最终会失败。
3.2 移动鼠标
-
mousedown/mouseup
: 在元素上点击 / 释放。 -
mouseover/mouseout
: 从一个元素上移入 / 移出。
3.2.1 事件属性 - event.relatedTarget
relatedTarget
属性是对target
的补充。relatedTarget
的值可以为null
,表明可能是鼠标从另一个窗口过来(over)、或移动到了另一个窗口上(out)。
当鼠标从 A 元素离开,已经移动到了 B 元素时:
-
对于
mouseover
: -
event.target
:鼠标移到的当前元素 —— B 元素。 -
event.relatedTarget
:鼠标之前所处的元素 —— A 元素。 -
对于
mouseout
,与 over 相反,记住 over 就行:event.target
:鼠标之前所处的元素 —— A元素。event.relatedTarget
:鼠标移到的当前元素 —— B 元素。
记: target
属性是我们的主要目的,relatedTarget
属性是我们为了方便而增添的附加信息。
- 所以,对于
mouseover
我们主要关注的是也就是当前鼠标所处的位置(over),这个值自然是保存到target
中。
3.2.3 元素的跳过
mousemove
事件,是随着鼠标的移动而触发。浏览器会间隔很小的周期,不断的重复检查鼠标的坐标位置,用以确定是否触发 mousemove
事件。
- 通过
mousemove
事件 ,浏览器就可以计算出mouseover
事件;通过mouseover
事件,浏览器就可以监听到mouseout
事件。
这意味着,当鼠标移动的速度非常快,可能在这个“小的周期”中,鼠标一下划过过了多个元素,这就会导致浏览器没有及时检测到鼠标具体划过了哪几个元素,造成了元素的跳过。
-
如果鼠标从上图所示的
#FROM
快速移动到#TO
元素,则中间的<div>
元素可能会被跳过。mouseout
事件可能会在#FROM
上被触发,然后立即在#TO
上触发mouseover
。 -
如果
mouseover
被触发了,则mouseout
也一定会触发,这两者是一一对应的。- 如果鼠标指针“正式地”进入了一个元素(生成了
mouseover
事件),那么一旦它离开,我们就会得到mouseout
。
- 如果鼠标指针“正式地”进入了一个元素(生成了
3.2.4 mouseover 的细节
先说原则:
- 鼠标指针移动到嵌套最多的那个元素上,也就是视觉上最突出的那个元素上(z-index最大的那个),就会触发
mouseover
事件。 - 可以非常笼统的说,在视觉上分割出的区域(子元素和父元素在视觉上是两个区域),鼠标在这两个区域移动,就会触发 over,out
以下分两种情况讨论:
3.2.4.1 父元素 ==> 子元素
当鼠标从父元素移动到子元素时,在父元素上就会触发 mouseout
事件,在子元素上就会触发 mouseover
事件。
- 如果设置了事件会发生捕获,则子元素上如果设置了
mouseover
事件,也会被触发。
3.2.4.2 子元素 ==> 父元素
当鼠标从子元素移动到父元素是,在子元素上就会触发 mouseout
事件,在父元素上就会触发 mouseover
事件。
- 与此同时,由于默认情况下事件会冒泡。因此,如果父元素上设置了
mouseout
的事件处理程序,也会触发mouseout
的回调。- 注意:此时触发的
mouseout
是归属于子元素的,所以虽然因冒泡在父元素上也触发了该事件,但其属性event.target
的值,依然同子元素上完全相同。
- 注意:此时触发的
因此,如果要判断鼠标是否离开了父元素和其嵌套的子元素,不能单单判断父元素上是否触发了 mouseout
,而是要具体判断:
-
event.target
的值是不是父元素。如果是,才能证明触发事件的元素,就是父元素本身。 -
或,
event.relatedTarget
的值是不是子元素。如果是,证明鼠标是从子元素移动到父元素上,而不是从外部移动到父元素。 -
或,
mouseenter
和mouseleave
事件。
3.2.5 mouseenter
和 mouseleave
事件 mouseenter/mouseleave
类似于 mouseover/mouseout
。它们在鼠标指针进入/离开元素时触发。
但是有两个重要的区别:
- enter 和 leave 事件,元素内部与后代之间的转换不会产生影响。
- 同时,事件
mouseenter/mouseleave
不会冒泡。
当鼠标指针进入一个元素时,会触发 mouseenter
,当鼠标指针离开该元素时,事件 mouseleave
才会触发。
- 与 over/out 的显著区别,就是没有了子元素嵌套的概念。只要还处在父元素中,即便是进入了更深的子元素,也依然不会触发
mouseleave
直到完全离开的父元素,才会触发。
3.2.6 事件委托
利用 mouseover
和 mouseout
可以建立事件委托,简单的例子如下:
在列表的 <ul>
上设置 mouseover
监听,利用对 event.target
属性值,可以判断出当前鼠标在其子元素中的哪一个位置。
<ul id="test">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#test');
ul.onmouseover = function(event) {
let text = event.target.firstChild; // 获取li标签中包含的文本值
console.log(text); // 当鼠标移动到某个li中,就会监听到,然后在控制台输出文本值:"1", "2"或"3"。
}
</script>
相反,mouseenter
和 mouseleave
由于忽略了父子元素的关系,不可以使用事件委托来监听。
3.3 拖放鼠标
3.3.1 算法
鼠标的拖放,简单来说就是三个步骤:鼠标按下、鼠标拖动、鼠标释放,对应了三个事件监听:mousedown
, mousemove
, mouseup
。
基础的拖放算法,在触发相关事件时,通常要做出如下行为:
mousedown
: 设置好准备移动的元素,可能是创建一个副本,也可能是设置他的position: absolute
。mousemove
:通过更改position:absolute
情况下的left/top
来移动它。mouseup
:执行与完成的拖放相关的所有行为。
有好几个应用,值得[反复记忆](鼠标拖放事件 (javascript.info))。
3.4 指针事件
3.4.1 历史
-
很早以前,只有鼠标事件。
-
引入了触摸事件。有了手机和平板电脑,触摸设备比鼠标具有更多的功能。例如,多点触控。鼠标事件并没有相关属性来处理这种多点触控。
例如
touchstart
、touchend
和touchmove
,它们具有特定于触摸的属性(这里不再赘述这些特性,因为指针事件更加完善)。不过这还是不够完美。很多输入设备(如触控笔)都有自己的特性。而且同时维护鼠标事件和触摸事件的代码,非常笨重。
-
引入了全新的规范「指针事件」。为各种指针输入设备提供了一套统一的事件。
注: IE 10 或 Safari 12 或更低的版本不兼容指针事件。
3.4.2 指针事件类型
指针事件的命名方式和鼠标事件类似:
指针事件 | 类似的鼠标事件 |
---|---|
pointerdown | mousedown |
pointerup | mouseup |
pointermove | mousemove |
pointerover | mouseover |
pointerout | mouseout |
pointerenter | mouseenter |
pointerleave | mouseleave |
pointercancel | - |
gotpointercapture | - |
lostpointercapture | - |
3.4.3 指针事件属性
指针事件具备和鼠标事件完全相同的属性,包括 clientX/Y
和 target
等。
以及一些其他属性:
pointerId
:触发当前事件的指针唯一标识符。浏览器生成的,解决多指针同时触发的问题。pointerType
:指针的设备类型,必须为字符串。可以是:“mouse”、“pen” 或 “touch”。- 我们可以针对不同类型的指针输入做出不同响应。
isPrimary
:当指针为首要指针 (多点触控时按下的第一根手指)时为true
。
有些指针设备会测量接触面积和点按压力(指压在触屏上),有很少使用的属性配合:
width
:指针(例如手指)接触设备的区域的宽度。对于不支持的设备(如鼠标),这个值总是1
。height
:指针(例如手指)接触设备的区域的长度。对于不支持的设备,这个值总是1
。pressure
:触摸压力,一个介于 0 到 1 之间的浮点数。对于不支持的设备,这个值总是0.5
(按下时)或0
(未按下时)。tangentialPressure
:归一化后的切向压力(tangential pressure)。tiltX
,tiltY
,twist
:针对触摸笔的几个属性,用于描述笔和屏幕表面的相对位置。
3.4.4 多点触控
我们可以通过 pointerId
和 isPrimary
属性的帮助,处理多点触控。
当用户用一根手指触摸在触摸屏的某个位置,然后将另一根手指放在该触摸屏的其他位置时,会发生以下情况:
- 第一个手指触摸:
pointerdown
事件触发,isPrimary=true
,并且被指派了一个pointerId
。
- 第二个和后续的更多个手指触摸(假设第一个手指仍在触摸):
pointerdown
事件触发,isPrimary=false
,并且每一个触摸都被指派了不同的pointerId
。
最终,如果有五个手指放在了屏幕上,我们会得到 5 个pointerdown
事件,和 5 个pointerId
。
3.4.1 指针中断 - pointercancel
pointercancel
事件在触发后,会取消当前处在活跃状态的指针。该事件常常用在主动中断指针,使被中断的指针不会继续触发其他指针事件:
导致指针中断的可能原因如下:
- 指针设备硬件在物理层面上被禁用。
- 设备方向旋转(例如给平板转了个方向)。
- 浏览器开始处理这一交互。比如将其看作是一个专门的鼠标手势或缩放操作等。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
pointercancel
事件。 - 我们可以通过阻止浏览器默认行为,来防止
pointercancel
事件的触发。
- 通常,一个对物体的拖拽操作,浏览器就会接管,主动触发
如何阻止阻止浏览器默认行为,来防止 pointercancel
事件的触发:
- 阻止原生的拖放操作发生:
- JS 中设置:
someElement.ondragstart = () => false
,也适用于鼠标事件。
- JS 中设置:
- 阻止其他触摸相关的浏览器默认操作:
- CSS 中设置:
#someElement { touch-action: none }
来阻止它们。
- CSS 中设置:
1.1.2 指针捕获 - setPointerCapture()
指针捕获允许一个特定的指针事件(PointerEvent
) 事件从一个事件触发时候的目标重定位到另一个目标上。这个功能可以确保一个元素可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素(比如,在滚动的时候)。
比如,在设置拖动一个小方块(box)的时候,指针事件在 document 上监听,一旦监听到指针处在 box 上时,可以使用指针捕获 (setPointerCapture
) 把 event.target
重定向(指向)到 box 上,这样的好处有:
- 其他元素将不能再作为该 pointer 事件的目标了,其他元素的
pointerover
,pointerout
pointerenter
, 和pointerleave
事件将不会被触发。接下来所有的指针事件,都会被重定向到 box 上。 - 确保 box 可以持续的接收到一个pointer事件,即使这个事件的触发点已经移出了这个元素。 比如在拖动划动条,鼠标经常会离开划动块儿的区域。利用指针捕获可以确保指向 box 的 pointer 事件一直在活跃状态。
- 即使用户在整个文档上移动指针,事件处理程序也将仅在
thumb
上被调用。 此外,事件对象的坐标属性,例如clientX/clientY
仍将是正确的,捕获仅影响target/currentTarget
。
语法:
elem.setPointerCapture(pointerId)
:指针捕获。
- 将给定的
pointerId
绑定到elem
。 在调用之后,所有具有相同pointerId
的指针事件,都将elem
作为目标(就像事件发生在elem
上一样),无论elem
在文档中的实际位置是什么。
elem.releasePointerCapture(pointerId)
:取消指针捕获。
绑定会在以下情况下被移除:
- 当
pointerup
或pointercancel
事件出现时; - 当
elem
被从文档中移除后; - 当
elem.releasePointerCapture(pointerId)
被调用后。
3.5 键盘事件
keydown
事件:当一个按键被按下时触发;
keyup
事件:当一个按键被释放时触发。
3.5.1 事件对象
event.key
属性:获取当前按键的字符,会受大小写 (shift) 的影响而保存不同字母。
event.code
属性:获取当前按键的“物理按键代码”。和按键一一对应,不会改变。
- 区分,
event.code
准确地标明了哪个键被按下。如两个 Shift 键,会区分"ShiftRight"
,"ShiftLeft"
。event.key
只标明按键的“含义”,即它是什么(一个“Shift”
),随着OS不同会因此改变:cmd
。
比如,按键 “Z” 的效果:
Key | event.key | event.code |
---|---|---|
Z | z (小写) | KeyZ |
Shift+Z | Z (大写) | KeyZ |
更多举例:
Key | event.key | event.code |
---|---|---|
F1 | F1 | F1 |
Backspace | Backspace | Backspace |
Shift | Shift | ShiftRight 或 ShiftLeft |
event.code
按键代码:
-
字符键:
"Key<letter>"
:"KeyA"
,"KeyB"
等。 -
数字键:
"Digit<number>"
:"Digit0"
,"Digit1"
等。- 特殊按键,为按键的名字:
"Enter"
,"Backspace"
,"Tab"
,"ShiftLeft"
等。
- 特殊按键,为按键的名字:
-
更多:UI 事件代码规范 。
3.5.2 兼容性问题
event.key
会受到不同OS平台的影响,而呈现不同的效果。例如在使用“撤销”组合按下时:
- MacOS:是
Cmd + Z
。 - Windows:是
Ctrl + Z
。
event.code
会受到不同键盘布局的影响,相同的按键位置却收到不同的结果,同样在“撤销”组合按下时:
- 美式布局 (QWERTY):是正常的,按下 Z 时,
event.code
等于KeyZ
。 - 德式布局 (QWERTZ):按下 Y 时,
event.code
也等于KeyZ
。
因此,event.code
可能由于特殊键盘布局,会错误的匹配字符。幸运的是,这种情况只发生在几个代码上,例如 keyA
,keyQ
,keyZ
,可以在 规范 中找到该列表。
总结:
-
如果频繁切换语言(德式键盘、美式键盘),使用
event.key
更好; -
如果想兼容更多操作系统(MacOS、Win),使用
event.code
更好。
3.5.3 自动重复
触发自动重复, event.repeat
属性会被设置为 true
。
如果按下一个键足够长的时间,它就会开始“自动重复”:
-
keydown
会被一次又一次地触发; -
当按键被释放时,最终会得到
keyup
。因此,有很多keydown
却只有一个keyup
是很正常的。 -
同时,对于由自动重复触发的事件,
event
对象的event.repeat
属性被设置为true
。